﻿namespace MvcScriptManager
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Globalization;
    using System.IO;
    using System.IO.Compression;
    using System.Reflection;
    using System.Resources;
    using System.Security.Permissions;
    using System.Text;
    using System.Threading;
    using System.Web;
    using System.Web.Caching;
    using System.Web.UI;
    using MvcScriptManager.UniqueKeyEncoding;

    /// <summary>
    /// ScriptManager for ASP.NET MVC Framework.
    /// Allows combining multiple scripts into one larger one as a way
    /// to reduce the number of files the client must download.
    /// </summary>
    [DefaultProperty("Scripts"), NonVisualControl, ParseChildren(true), PersistChildren(false), AspNetHostingPermission(SecurityAction.InheritanceDemand, Level = AspNetHostingPermissionLevel.Minimal), AspNetHostingPermission(SecurityAction.LinkDemand, Level = AspNetHostingPermissionLevel.Minimal)]
    public class ScriptManager : Control
    {
        #region Constants

        /// <summary>
        /// Request parameter name for combined scripts.
        /// </summary>
        private const string CombinedScriptsParamName = "s";

        /// <summary>
        /// Request parameter name for debug mode indication.
        /// </summary>
        private const string DebugParamName = "d";

        /// <summary>
        /// Request parameter name for script token setting.
        /// </summary>
        private const string TokenParamName = "v";

        /// <summary>
        /// Request parameter name for HTTP compression setting.
        /// </summary>
        private const string CompressionParamName = "c";

        /// <summary>
        /// Request parameter name for HTTP expiration header setting.
        /// </summary>
        private const string ExpirationParamName = "e";

        /// <summary>
        /// Request parameter name for HTTP cacheability settings.
        /// </summary>
        private const string HttpCacheabilityParamName = "ca";

        /// <summary>
        /// Request parameter name for HTTP expiration header days setting.
        /// </summary>
        private const string ExpirationDaysParamName = "ed";

        /// <summary>
        /// Request parameter name for the Unique Key Encoding Scheme setting
        /// </summary>
        private const string UniqueKeyEncodingSchemeParamName = "es";

        /// <summary>
        /// Javascript MIME type. The value should be 'application/javascript',
        /// but Internet Explorer has issues, so use 'text/javascript' for compatibility
        /// </summary>
        private const string JavascriptMimeType = "text/javascript";

        /// <summary>
        /// Default cacheablity is Public, as it maintains backward compatibiliy with
        /// previous versions of the code that did not have the option. It also is a
        /// sensible setting, unless your javascript code frequently changes.
        /// </summary>
        private const HttpCacheability DefaultCacheability = HttpCacheability.Public;

        #endregion

        #region Private Static fields

        /// <summary>
        /// For now only use YuiJavaScriptCompressor.
        /// </summary>
        private static readonly IScriptMinifier ScriptMinifier = new YuiJavaScriptCompressor();

        /// <summary>
        /// Static object used for locking.
        /// </summary>
        private static readonly object SyncRoot = new object();

        #endregion

        #region Private fields

        /// <summary>
        /// CombineScriptHandler URL default value.
        /// </summary>
        private string combineScriptsHandlerUrl = "~/CombineScriptHandler.aspx";

        /// <summary>
        /// A value indicating if Script Combining should be disabled
        /// while debugging
        /// </summary>
        private bool disableCombiningWhileDebugging = true;

        /// <summary>
        /// A value indicating if HTTP compression should be enabled
        /// when outputing combined script file.
        /// </summary>
        private bool enableHttpCompression = true;

        /// <summary>
        /// A value indicating if HTTP expiration header should be added
        /// when outputing combined script file.
        /// </summary>
        private bool enableHttpExpiration = true;

        /// <summary>
        /// HTTP cacheability settings to control both client and server
        /// side caching.
        /// </summary>
        private HttpCacheability httpCacheability = DefaultCacheability;

        /// <summary>
        /// A script token to be appended to query string
        /// when generating combined script URL.
        /// </summary>
        private string scriptToken;

        /// <summary>
        /// HTTP expiration header setting when outputing combined script file.
        /// </summary>
        private int httpExpirationInDays = 365;

        /// <summary>
        /// Rendering mode for the current ScriptManager instance.
        /// </summary>
        private RenderMode renderMode = RenderMode.Master;

        private ScriptReferenceCollection scripts;

        /// <summary>
        /// The encoding scheme for the unique keys of any combined files
        /// </summary>
        private UniqueKeyEncodingScheme uniqueKeyEncodingScheme = UniqueKeyEncodingScheme.TwoWayEncoding;

        #endregion

        #region Public properties

        /// <summary>
        /// Gets a value indicating whether debugging is enabled.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if debugging is enabled; otherwise, <c>false</c>.
        /// </value>
        [Browsable(false)]
        public static bool IsDebuggingEnabled
        {
            get
            {
                if (DeploymentSectionCache.Instance.Retail)
                {
                    return false;
                }

                return AppLevelCompilationSectionCache.Instance.Debug;
            }
        }

        /// <summary>
        /// Gets the collection of scripts.
        /// </summary>
        /// <remarks>
        /// If you add scripts programmatically to this collection, make sure you add them
        /// before PreRender of the page or control.
        /// </remarks>
        /// <value>The collection of scripts.</value>
        [MergableProperty(false), PersistenceMode(PersistenceMode.InnerDefaultProperty), Category("Behavior"), DefaultValue((string)null), DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
        public ScriptReferenceCollection Scripts
        {
            get
            {
                if (this.scripts == null)
                {
                    this.scripts = new ScriptReferenceCollection();
                }

                return this.scripts;
            }
        }

        /// <summary>
        /// Gets or sets the URL of the HTTP handler for generating the combined script files.
        /// </summary>
        /// <value>The URL of the HTTP handler.</value>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "Needs to be string")]
        [UrlProperty, DefaultValue("~/CombineScriptHandler.aspx"), Category("Behavior")]
        public string CombineScriptsHandlerUrl
        {
            get
            {
                return this.combineScriptsHandlerUrl;
            }

            set
            {
                this.combineScriptsHandlerUrl = value;
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether Script Combining is applied while debugging.
        /// This parameter only applies to Master instances.
        /// If <c>true</c> (the default) one &lt;script&gt; tag will be written for each script reference
        /// Set to <c>false</c> to test Script Combining.
        /// </summary>
        /// <value>
        /// 	<c>true</c> (default) if Script Combining should be disabled while debugging; otherwise, <c>false</c>.
        /// </value>
        [DefaultValue(true), Category("Behavior")]
        public bool DisableCombiningWhileDebugging
        {
            get
            {
                return this.disableCombiningWhileDebugging;
            }

            set
            {
                this.disableCombiningWhileDebugging = value;
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether HTTP compression should be enabled
        /// when outputing scripts. Default is true.
        /// Set to false if you can enable HTTP compression on web server or load balancer.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if HTTP compression should be enabled; otherwise, <c>false</c>.
        /// </value>
        [DefaultValue(true), Category("Behavior")]
        public bool EnableHttpCompression
        {
            get
            {
                return this.enableHttpCompression;
            }

            set
            {
                this.enableHttpCompression = value;
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether HTTP expiration header should be aded
        /// when outputing scripts. Default is true.
        /// Set to false if you can add the header on web server or load balancer.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if HTTP expiration header should be added; otherwise, <c>false</c>.
        /// </value>
        [DefaultValue(true), Category("Behavior")]
        public bool EnableHttpExpiration
        {
            get
            {
                return this.enableHttpExpiration;
            }

            set
            {
                this.enableHttpExpiration = value;
            }
        }

        /// <summary>
        /// Gets or sets the HTTP expiration in days.
        /// This setting will only be in effect when EnableHttpExpiration is set to true.
        /// </summary>
        /// <value>The HTTP expiration in days.</value>
        [DefaultValue(365), Category("Behavior")]
        public int HttpExpirationInDays
        {
            get
            {
                return this.httpExpirationInDays;
            }

            set
            {
                this.httpExpirationInDays = value;
            }
        }

        /// <summary>
        /// Gets or sets the HTTP cacheability.
        /// This setting allows more control over whether the client and server
        /// can cache the javascript produced in the script combiner.
        /// </summary>
        /// <value>The HTTP cacheability setting.</value>
        [DefaultValue(DefaultCacheability), Category("Behavior")]
        public System.Web.HttpCacheability HttpCacheability
        {
            get
            {
                return this.httpCacheability;
            }

            set
            {
                this.httpCacheability = value;
            }
        }

        /// <summary>
        /// Gets or sets a value to be appended to query string when generating combined script URL.
        /// When set，it will force the previous browser cache on the script to expire under release/production mode.
        /// Note that under debug mode, no need to add this setting because a built-in debug param is added
        /// automatically to each combined script request.
        /// </summary>
        /// <value>The script token.</value>
        [DefaultValue(""), Category("Behavior")]
        public string ScriptToken
        {
            get
            {
                return this.scriptToken;
            }

            set
            {
                this.scriptToken = value;
            }
        }

        /// <summary>
        /// Gets or sets the rendering mode for the current ScriptManager instance.
        /// Master - the current instance will render <Script /> tag.
        /// Slave - the current instance will not render. Instead, it will serve as a
        /// container for appending new scripts to the preceding Master.
        /// The default value is Master.
        /// This property is useful, especially when you have one or more ScriptManagers
        /// declared in the master page(s), and you are declaring another ScriptManager
        /// in the specific (non-master) page to add more scripts, but you want to hand
        /// over the rendering to the ScriptManager inside the master page only. In this
        /// case, you can set the ScriptManager's RenderMode in master page to Master, and
        /// the ScriptManager's RenderMode in the specific page to Slave.
        /// </summary>
        /// <value>The rendering mode.</value>
        [DefaultValue(RenderMode.Master), Category("Behavior")]
        public RenderMode RenderMode
        {
            get
            {
                return this.renderMode;
            }

            set
            {
                this.renderMode = value;
            }
        }

        /// <summary>
        /// Gets or sets the encoding scheme used for the file reference keys. Defaults to use two-way hashes.
        /// See <see cref="UniqueKeyEncodingScheme"/> for the available options. If the ScriptManager has its
        /// <see cref="RenderMode"/> property set to Slave, this property will be ignored and the value on
        /// the master ScriptManager will be used.
        /// </summary>
        [DefaultValue(UniqueKeyEncodingScheme.TwoWayEncoding), Category("Behavior")]
        public UniqueKeyEncodingScheme UniqueKeyEncodingScheme
        {
            get
            {
                return this.uniqueKeyEncodingScheme;
            }

            set
            {
                this.uniqueKeyEncodingScheme = value;
            }
        }

        #endregion

        /// <summary>
        /// Gets the current ScriptManager instance from the page.
        /// </summary>
        /// <param name="page">The page in which the ScriptManager control resides.</param>
        /// <returns>The current ScriptManager instance.</returns>
        public static ScriptManager GetCurrent(Page page)
        {
            if (page == null)
            {
                throw new ArgumentNullException("page");
            }

            ScriptManager scriptManager = page.Items[typeof(ScriptManager)] as ScriptManager;
            return scriptManager;
        }

        /// <summary>
        /// Outputs the combined script file.
        /// </summary>
        /// <param name="context">HttpContext for the transaction.</param>
        /// <returns>true if the script file output is succesful.</returns>
        public static bool OutputCombinedScriptFile(HttpContext context)
        {
            bool output = false;
            HttpRequest request = context.Request;
            string combinedScripts = request.Params[CombinedScriptsParamName];
            string debug = request.Params[DebugParamName];
            bool compression = request.Params[CompressionParamName] != null;
            bool expiration = request.Params[ExpirationParamName] != null;
            HttpCacheability httpCacheability = DefaultCacheability;
            if (request.Params[HttpCacheabilityParamName] != null)
            {
                httpCacheability = (HttpCacheability)Enum.Parse(typeof(HttpCacheability), request.Params[HttpCacheabilityParamName]);
            }

            // Get the scheme used for encoding the unique keys
            int uniqueKeyEncodingSchemeParamValue;
            int.TryParse(request.Params[UniqueKeyEncodingSchemeParamName], out uniqueKeyEncodingSchemeParamValue);
            UniqueKeyEncodingScheme uniqueKeyEncodingScheme = (UniqueKeyEncodingScheme)uniqueKeyEncodingSchemeParamValue;

            if (!string.IsNullOrEmpty(combinedScripts))
            {
                // Get the script keys present
                string[] scriptKeys = HttpUtility.UrlDecode(combinedScripts).Split(
                        new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);

                // This is a request for a combined script file
                HttpResponse response = context.Response;
                response.ContentType = JavascriptMimeType;

                // Get the appropriate unique key scheme to use
                UniqueKeyEncoderFactory uniqueKeyEncoderFactory = new UniqueKeyEncoderFactory();
                IUniqueKeyEncoder uniqueKeyEncoder =
                        uniqueKeyEncoderFactory.GetEncoderForScheme(uniqueKeyEncodingScheme);

                // Issue #1308 - resolves Last-Modified and ETag header problem
                // Get the file paths so that we can add them as dependencies to the response
                foreach (string scriptKey in scriptKeys)
                {
                    // Using the appropriate unique key scheme, get hold of the ScriptReference
                    ScriptReference reference = uniqueKeyEncoder.DecodeUniqueKey(scriptKey, context);

                    // Add the script as a dependency to the response
                    response.AddFileDependency(context.Server.MapPath(reference.Path));
                }

                // Set the caching rules
                HttpCachePolicy cache = response.Cache;
                cache.SetCacheability(httpCacheability);
                cache.VaryByParams[CombinedScriptsParamName] = true;
                cache.SetOmitVaryStar(true);

                // Set the Last-Modified header based on all of the script files
                cache.SetLastModifiedFromFileDependencies();

                // Set the ETag header
                cache.SetETagFromFileDependencies();

                if (!string.IsNullOrEmpty(debug))
                {
                    cache.VaryByParams[DebugParamName] = true;
                }

                if (request.Params[TokenParamName] != null)
                {
                    cache.VaryByParams[TokenParamName] = true;
                }

                if (expiration)
                {
                    int days;

                    // Fixed issue #1437
                    if (!int.TryParse(request.Params[ExpirationDaysParamName], out days))
                    {
                        days = 365;
                    }
                    else
                    {
                        if (days <= 0)
                        {
                            days = 365;
                        }
                    }

                    cache.SetExpires(DateTime.Now.AddDays(days));
                    cache.SetValidUntilExpires(true);
                }

                // Get the stream to write the combined script to (using a compressed stream if requested)
                // Note that certain versions of IE6 have difficulty with compressed responses, so we
                // don't compress for those browsers (just like ASP.NET AJAX's ScriptResourceHandler)
                Stream outputStream = response.OutputStream;
                if (compression && (!request.Browser.IsBrowser("IE") || (6 < request.Browser.MajorVersion)))
                {
                    foreach (string acceptEncoding in (request.Headers["Accept-Encoding"] ?? string.Empty).ToUpperInvariant().Split(','))
                    {
                        if ("GZIP" == acceptEncoding)
                        {
                            // Browser wants GZIP; wrap the output stream with a GZipStream
                            response.AddHeader("Content-encoding", "gzip");
                            outputStream = new GZipStream(outputStream, CompressionMode.Compress);
                            break;
                        }

                        if ("DEFLATE" == acceptEncoding)
                        {
                            // Browser wants Deflate; wrap the output stream with a DeflateStream
                            response.AddHeader("Content-encoding", "deflate");
                            outputStream = new DeflateStream(outputStream, CompressionMode.Compress);
                            break;
                        }
                    }
                }

                // Output the combined script
                using (StreamWriter outputWriter = new StreamWriter(outputStream))
                {
                    // Write the scripts
                    WriteScripts(scriptKeys, outputWriter, context, uniqueKeyEncoder);
                }

                output = true;
            }

            return output;
        }

        /// <summary>
        /// Raises the <see cref="System.Web.UI.Control.Init"/> event.
        /// </summary>
        /// <param name="e">An <see cref="System.EventArgs"/> object that contains the event data.</param>
        protected override void OnInit(EventArgs e)
        {
            if (string.IsNullOrEmpty(this.CombineScriptsHandlerUrl))
            {
                throw new InvalidOperationException("CombineScriptsHandlerUrl value is required.");
            }

            base.OnInit(e);

            if (this.DesignMode)
            {
                return;
            }

            // Update the context to store the current instance if its rendering mode is Master.
            // If there are multiple Master instances, the context always references the last Master instance.
            if (this.renderMode == RenderMode.Master)
            {
                this.Page.Items[typeof(ScriptManager)] = this;
            }
            else
            {
                // We need to fail this when a slave doesn't come with a preceding master
                ScriptManager scriptManager = this.Page.Items[typeof(ScriptManager)] as ScriptManager;
                if (scriptManager == null)
                {
                    throw new InvalidOperationException("A ScriptManager with RenderMode set to Master is not present. Such ScriptManager must precede one with RenderMode set to Slave.");
                }
            }
        }

        /// <summary>
        /// Raises the <see cref="System.Web.UI.Control.PreRender"/> event.
        /// </summary>
        /// <param name="e">An <see cref="System.EventArgs"/> object that contains the event data.</param>
        protected override void OnPreRender(EventArgs e)
        {
            base.OnPreRender(e);

            // When an instance with Slave RenderMode is encountered, we need to copy its scripts to the Master
            // instance we store previously in the context, so that we the Master isntance is rendered, it will
            // pick up these scripts.
            if (this.renderMode == RenderMode.Slave)
            {
                ScriptManager scriptManager = (ScriptManager)this.Page.Items[typeof(ScriptManager)];
                foreach (ScriptReference reference in this.Scripts)
                {
                    scriptManager.Scripts.Add(reference);
                }
            }
        }

        /// <summary>
        /// Sends server control content to a provided <see cref="System.Web.UI.HtmlTextWriter"/> object,
        /// which writes the content to be rendered on the client.
        /// </summary>
        /// <param name="writer">The <see cref="System.Web.UI.HtmlTextWriter"/> object that receives
        /// the server control content.</param>
        protected override void Render(HtmlTextWriter writer)
        {
            base.Render(writer);

            // Only render <Script /> tag when the RenderMode is Master
            if (this.renderMode == RenderMode.Master)
            {
                // In Debug mode, render each script tag independently
                if (IsDebuggingEnabled && DisableCombiningWhileDebugging)
                {
                    foreach (ScriptReference scriptReference in this.Scripts)
                    {
                        writer.AddAttribute(HtmlTextWriterAttribute.Src, scriptReference.GetUrl(HttpContext.Current, true));
                        writer.AddAttribute(HtmlTextWriterAttribute.Type, JavascriptMimeType);
                        writer.RenderBeginTag(HtmlTextWriterTag.Script);
                        writer.RenderEndTag();
                    }

                    return;
                }

                StringBuilder scriptSrc =
                        new StringBuilder(
                                string.Format(
                                        CultureInfo.InvariantCulture,
                                        "{0}?{1}={2}&{3}={4}",
                                        this.ResolveUrl(this.combineScriptsHandlerUrl),
                                        CombinedScriptsParamName,
                                        HttpUtility.UrlEncode(this.SerializeScripts()),
                                        UniqueKeyEncodingSchemeParamName,
                                        (int)this.uniqueKeyEncodingScheme));

                if (this.EnableHttpCompression)
                {
                    scriptSrc.AppendFormat(
                        CultureInfo.InvariantCulture,
                        "&{0}={1}",
                        CompressionParamName,
                        "1");
                }

                if (this.EnableHttpExpiration)
                {
                    scriptSrc.AppendFormat(
                        CultureInfo.InvariantCulture,
                        "&{0}={1}",
                        ExpirationParamName,
                        "1");
                    scriptSrc.AppendFormat(
                        CultureInfo.InvariantCulture,
                        "&{0}={1}",
                        ExpirationDaysParamName,
                        this.HttpExpirationInDays);
                    
                }

                if (this.HttpCacheability != DefaultCacheability)
                {
                    scriptSrc.AppendFormat(
                        CultureInfo.InvariantCulture,
                        "&{0}={1}",
                        HttpCacheabilityParamName,
                        (int)this.httpCacheability);
                }

                if (IsDebuggingEnabled)
                {
                    // Append unique token under debug mode
                    scriptSrc.AppendFormat(
                        CultureInfo.InvariantCulture,
                        "&{0}={1}",
                        DebugParamName,
                        Guid.NewGuid().ToString("N"));
                }
                else if (!string.IsNullOrEmpty(this.ScriptToken))
                {
                    // Append version token
                    scriptSrc.AppendFormat(
                        CultureInfo.InvariantCulture,
                        "&{0}={1}",
                        TokenParamName,
                        this.ScriptToken);
                }

                writer.AddAttribute(HtmlTextWriterAttribute.Src, scriptSrc.ToString());
                writer.AddAttribute(HtmlTextWriterAttribute.Type, JavascriptMimeType);
                writer.RenderBeginTag(HtmlTextWriterTag.Script);
                writer.RenderEndTag();
            }
        }

        /// <summary>
        /// Writes scripts (including localized script resources) to the specified stream.
        /// </summary>
        /// <param name="scriptKeys">The script hash keys.</param>
        /// <param name="outputWriter">The output writer.</param>
        /// <param name="context">The HTTP context.</param>
        /// <param name="uniqueKeyEncoder">The unique key encoder to use</param>
        private static void WriteScripts(IEnumerable<string> scriptKeys, TextWriter outputWriter, HttpContext context, IUniqueKeyEncoder uniqueKeyEncoder)
        {
            CultureInfo currentUiCulture = Thread.CurrentThread.CurrentUICulture;
            foreach (string scriptKey in scriptKeys)
            {
                // Write out the associated script resources (if any) in the proper culture
                ScriptReference reference = uniqueKeyEncoder.DecodeUniqueKey(scriptKey, context);

                string assemblyName = reference.ResourceAssembly;
                string resourceName = reference.ResourceName;
                string resourceTypeName = reference.ResourceTypeName;
                if (!string.IsNullOrEmpty(assemblyName) && !string.IsNullOrEmpty(resourceName) && !string.IsNullOrEmpty(resourceTypeName))
                {
                    Assembly resourceAssembly = Assembly.Load(assemblyName);
                    outputWriter.Write(string.Format(CultureInfo.InvariantCulture, "{0}={{", resourceTypeName));

                    // Get the script resource name (without the trailing ".resources")
                    if (resourceName.EndsWith(".resources", StringComparison.OrdinalIgnoreCase))
                    {
                        resourceName = resourceName.Substring(0, resourceName.Length - 10);
                    }

                    // Load a ResourceManager/ResourceSet and walk through the list to output them all
                    ResourceManager resourceManager = new ResourceManager(resourceName, resourceAssembly);
                    using (ResourceSet resourceSet = resourceManager.GetResourceSet(CultureInfo.InvariantCulture, true, true))
                    {
                        bool first = true;
                        foreach (DictionaryEntry de in resourceSet)
                        {
                            if (!first)
                            {
                                // Need a comma between all entries
                                outputWriter.Write(",");
                            }

                            // Output the entry
                            string name = (string)de.Key;
                            string value = resourceManager.GetString(name, currentUiCulture);
                            outputWriter.Write(string.Format(CultureInfo.InvariantCulture, "\"{0}\":\"{1}\"", QuoteString(name), QuoteString(value)));
                            first = false;
                        }
                    }

                    outputWriter.Write("};" + Environment.NewLine);
                }

                outputWriter.WriteLine(GetScript(scriptKey, context, uniqueKeyEncoder));
            }
        }

        /// <summary>
        /// Gets the script content.
        /// </summary>
        /// <param name="scriptKey">The script hash key.</param>
        /// <param name="context">The HTTP context.</param>
        /// <param name="uniqueKeyEncoder">The unique key encoder to use</param>
        /// <returns>The script content.</returns>
        private static string GetScript(string scriptKey, HttpContext context, IUniqueKeyEncoder uniqueKeyEncoder)
        {
            if (context.Cache[scriptKey] == null)
            {
                lock (SyncRoot)
                {
                    if (context.Cache[scriptKey] == null)
                    {
                        ScriptReference reference = uniqueKeyEncoder.DecodeUniqueKey(scriptKey, context);

                        string path = reference.Path;
                        string localPath = ScriptReference.GetLocalPath(path, context);
                        string scriptContent = ScriptReference.GetScript(localPath);

                        if (!IsDebuggingEnabled && reference.EnableCrunching)
                        {
                            // Only minify script in release mode
                            scriptContent = ScriptMinifier.Minify(scriptContent);
                        }

                        // Add cache entry and file dependency
                        context.Cache.Insert(
                            scriptKey,
                            scriptContent,
                            new CacheDependency(localPath),
                            Cache.NoAbsoluteExpiration,
                            Cache.NoSlidingExpiration,
                            CacheItemPriority.Default,
                            null);
                    }
                }
            }

            return (string)context.Cache[scriptKey];
        }

        /// <summary>
        /// Callable implementation of System.Web.Script.Serialization.JavaScriptString.QuoteString.
        /// </summary>
        /// <param name="value">Value to quote.</param>
        /// <returns>The quoted string.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Callable implementation of System.Web.Script.Serialization.JavaScriptString.QuoteString")]
        private static string QuoteString(string value)
        {
            StringBuilder builder = null;
            if (string.IsNullOrEmpty(value))
            {
                return string.Empty;
            }

            int startIndex = 0;
            int count = 0;
            for (int i = 0; i < value.Length; i++)
            {
                char c = value[i];
                if ((((c == '\r') || (c == '\t')) || ((c == '"') || (c == '\''))) || ((((c == '<') || (c == '>')) || ((c == '\\') || (c == '\n'))) || (((c == '\b') || (c == '\f')) || (c < ' '))))
                {
                    if (builder == null)
                    {
                        builder = new StringBuilder(value.Length + 5);
                    }

                    if (count > 0)
                    {
                        builder.Append(value, startIndex, count);
                    }

                    startIndex = i + 1;
                    count = 0;
                }

                switch (c)
                {
                    case '<':
                    case '>':
                    case '\'':
                        {
                            AppendCharacterAsUnicode(builder, c);
                            continue;
                        }

                    case '\\':
                        {
                            builder.Append(@"\\");
                            continue;
                        }

                    case '\b':
                        {
                            builder.Append(@"\b");
                            continue;
                        }

                    case '\t':
                        {
                            builder.Append(@"\t");
                            continue;
                        }

                    case '\n':
                        {
                            builder.Append(@"\n");
                            continue;
                        }

                    case '\f':
                        {
                            builder.Append(@"\f");
                            continue;
                        }

                    case '\r':
                        {
                            builder.Append(@"\r");
                            continue;
                        }

                    case '"':
                        {
                            builder.Append("\\\"");
                            continue;
                        }
                }

                if (c < ' ')
                {
                    AppendCharacterAsUnicode(builder, c);
                }
                else
                {
                    count++;
                }
            }

            if (builder == null)
            {
                return value;
            }

            if (count > 0)
            {
                builder.Append(value, startIndex, count);
            }

            return builder.ToString();
        }

        /// <summary>
        /// Callable implementation of System.Web.Script.Serialization.JavaScriptString.AppendCharAsUnicode.
        /// </summary>
        /// <param name="builder">The string builder.</param>
        /// <param name="character">The character to append.</param>
        private static void AppendCharacterAsUnicode(StringBuilder builder, char character)
        {
            builder.Append(@"\u");
            builder.AppendFormat(CultureInfo.InvariantCulture, "{0:x4}", new object[] { (int)character });
        }

        /// <summary>
        /// Serializes the collection of scripts to string.
        /// </summary>
        /// <returns>The serilized string.</returns>
        private string SerializeScripts()
        {
            List<string> uniqueScripts = new List<string>();

            // Get the unique key scheme to use
            UniqueKeyEncoderFactory uniqueKeyEncoderFactory = new UniqueKeyEncoderFactory();
            IUniqueKeyEncoder uniqueKeyEncoder = uniqueKeyEncoderFactory.GetEncoderForScheme(this.UniqueKeyEncodingScheme);

            foreach (ScriptReference scriptReference in this.Scripts)
            {
                // Build the unique key for this script reference
                string uniqueKey = uniqueKeyEncoder.EncodeUniqueKey(scriptReference, HttpContext.Current);

                if (!uniqueScripts.Contains(uniqueKey))
                {
                    uniqueScripts.Add(uniqueKey);
                }
            }

            return string.Join(";", uniqueScripts.ToArray());
        }
    }
}
